package org.simplextensions.graph;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.*;
import java.util.concurrent.ConcurrentLinkedQueue;

/**
 * 
 * @author Tomasz Krzyzak, <a
 *         href="mailto:tomasz.krzyzak@gmail.com">tomasz.krzyzak@gmail.com</a>
 * @since 2010-04-01 22:53:42
 */
public class Graph {

	private static final Logger log = LoggerFactory.getLogger(Graph.class);

	private Queue<IGraphEventListener> graphListeners = new ConcurrentLinkedQueue<IGraphEventListener>();

	private Map<String, GraphNode> nodesMap = new HashMap<String, GraphNode>();

	private Map<Object, GraphNode> nodesDataMap = new HashMap<Object, GraphNode>();

	private Map<String, Collection<GraphLink>> unendedConnectionsMap = new HashMap<String, Collection<GraphLink>>();

	private final String name;

	private static int counter = 1;

	public Graph() {
		this.name = "Graph" + counter++;
	}

	public Graph(String name) {
		this.name = name;
	}

	public String getName() {
		return name;
	}

	public void addNode(String id, String[] connections, Object data) throws NodeAlreadyExistsException {
		if (id == null)
			throw new NullPointerException(this.name + ": id cannot be null");

		if (nodesMap.get(id) != null)
			throw new NodeAlreadyExistsException(this.name + ": Node already exists: " + id);

		if (log.isDebugEnabled())
			log.debug(this.name + ": adding node: " + id);

		// adding new node to nodes list
		GraphNode newNode = new GraphNode(this, id, data);
		nodesMap.put(id, newNode);
		nodesDataMap.put(data, newNode);

		notifyNodeAdded(newNode);

		connections = trim(connections);

		Map<String, GraphLink> outgoingNodes = newNode.addOutgoing(connections);
		for (String endNodeId : outgoingNodes.keySet()) {
			// create connection
			GraphLink graphLink = outgoingNodes.get(endNodeId);
			GraphNode endNode = nodesMap.get(endNodeId);
			if (endNode != null)
				graphLink.setEndNode(endNode);

			// if endNode of this connection does not exists add connection
			// to unendedconnections set
			if (endNode == null) {
				Collection<GraphLink> unendedConnections = unendedConnectionsMap.get(endNodeId);
				if (unendedConnections == null) {
					unendedConnectionsMap.put(endNodeId, unendedConnections = new HashSet<GraphLink>());
				}
				unendedConnections.add(graphLink);
			}
		}
		// finding all links that was supposed to point to this new node.
		// removing these unended links
		Collection<GraphLink> set = unendedConnectionsMap.remove(id);
		if (set != null) {
			for (GraphLink graphLink : set) {
				graphLink.setEndNode(newNode);
			}
		}
	}

	private String[] trim(String[] connections) {
		List<String> result = new LinkedList<String>();
		if (connections != null)
			for (String s : connections) {
				if (s != null && !result.contains(s))
					result.add(s);
			}
		return result.toArray(new String[result.size()]);
	}

	public boolean removeNode(Object data) {
		GraphNode node2Remove = this.nodesDataMap.get(data);
		// check if node2Remove exists
		if (node2Remove == null)
			return false;
		// get all linkd point to this node
		Collection<GraphLink> incommingConnections = node2Remove.getIncommingLinks();
		// if reverse connections exist
		if (incommingConnections != null) {
			for (GraphLink gl : incommingConnections) {
				// disconnect gl from this node
				gl.setEndNode(null);
			}
			// every connection pointing to this node becomes unended connection
			unendedConnectionsMap.put(node2Remove.getId(), incommingConnections);
		}

		// removing node
		for (GraphLink link : node2Remove.getOutgoingLinks()) {
			GraphNode endNode = link.getEndNode();
			endNode.innerRemoveIncoming(link);
		}
		nodesMap.remove(node2Remove.getId());
		notifyNodeRemoved(nodesDataMap.remove(node2Remove.getObject()));

		return true;
	}

	public String[] getOutgoingNodesIds(Object data) {
		GraphNode node = this.nodesDataMap.get(data);
		if (node != null) {
			Set<String> result = new HashSet<String>();
			// get endNodeId of every link of node
			Collection<GraphLink> linksSet = node.getOutgoingLinks();
			for (GraphLink gl : linksSet) {
				result.add(gl.getEndNodeId());
			}
			return result.toArray(new String[result.size()]);
		}
		return null;
	}

	public void addGraphListener(IGraphEventListener listener) {
		if (!this.graphListeners.contains(listener))
			this.graphListeners.add(listener);
	}

	public void removeGraphListener(IGraphEventListener listener) {
		this.graphListeners.remove(listener);
	}

	public void notifyNodeAdded(GraphNode node) {
		GraphEvent graphEvent = new GraphEvent(node.getId(), node.getObject());
		for (IGraphEventListener listener : graphListeners) {
			try {
				listener.nodeAdded(graphEvent);
			} catch (Exception e) {
				log.warn("", e);
			}
		}
	}

	public void notifyNodeRemoved(GraphNode node) {
		GraphEvent graphEvent = new GraphEvent(node.getId(), node.getObject());
		for (IGraphEventListener listener : graphListeners) {
			try {
				listener.nodeRemoved(graphEvent);
			} catch (Exception e) {
				log.warn("", e);
			}
		}
	}

	public void notifyNodeFullyConnected(GraphNode node, Set<GraphNode> nodes) {
		if (nodes.contains(node))
			return;
		GraphEvent graphEvent = new GraphEvent(node.getId(), node.getObject());
		for (IGraphEventListener listener : graphListeners) {
			try {
				listener.nodeFullyConnected(graphEvent);
			} catch (Exception e) {
				log.warn("", e);
			}
		}
		nodes.add(node);
	}

	private void prettyPrintNotFullyConnected(GraphNode node) {
		Collection<GraphLink> set2 = node.getOutgoingLinks();
		// check all links of startNode of current link
		for (GraphLink gl2 : set2) {
			if (gl2.getEndNode() == null) {
				log.trace("\t\t" + gl2.getEndNodeId());
			} else if (!gl2.getEndNode().isFullyConnected()) {
				log.trace("\t\t*" + gl2.getEndNodeId());
			}
		}
	}

	public void notifyNodeLostConnections(GraphNode node, Set<GraphNode> nodes) {
		if (nodes.contains(node))
			return;

		if (log.isTraceEnabled()) {
			log.trace(this.name + ": node lost connections: " + node);
		}
		prettyPrintNotFullyConnected(node);

		GraphEvent graphEvent = new GraphEvent(node.getId(), node.getObject());
		for (IGraphEventListener listener : graphListeners) {
			try {
				listener.nodeLostConnections(graphEvent);
			} catch (Exception e) {
				log.warn("", e);
			}
		}
		nodes.add(node);
	}

	public Set<Object> getOutgoingNodes(Object node) {
		Set<Object> result = new HashSet<Object>();
		GraphNode graphNode = this.nodesDataMap.get(node);

		for (GraphLink gl : graphNode.getOutgoingLinks()) {
			GraphNode endNode = gl.getEndNode();
			if (endNode != null)
				result.add(endNode);
		}

		return result;
	}

	public Set<Object> getIncomingNodes(Object node) {
		Set<Object> result = new HashSet<Object>();
		GraphNode graphNode = this.nodesDataMap.get(node);

		for (GraphLink gl : graphNode.getIncommingLinks()) {
			GraphNode startNode = gl.getStartNode();
			result.add(startNode.getObject());
		}

		return result;
	}

	public void visitWithDependencies(Object node, IGraphVisitor visitor) {
		GraphNode graphNode = this.nodesDataMap.get(node);
		if (graphNode != null) {
			visitWithDependencies(graphNode, visitor);
		}
	}

	private void visitWithDependencies(GraphNode node, IGraphVisitor visitor) {
		if (node != null) {
			Collection<GraphLink> outgoingLinks = node.getOutgoingLinks();
			for (GraphLink gl : outgoingLinks) {
				visitWithDependencies(gl.getEndNode(), visitor);
			}
			visitor.visit(node.getObject());
		}
	}
	
	public boolean contains(Object node){
		return this.nodesDataMap.containsKey(node);
	}
}
